// AudioEngine.cs
//
// Copyright (c) 2007 Mike Strobel
//
// This source code is subject to the terms of the Microsoft Reciprocal License (Ms-RL).
// For details, see <http://www.opensource.org/licenses/ms-rl.html>.
//
// All other rights reserved.

using System;
using System.Collections.Generic;
using System.Concurrency;
using System.Diagnostics;
using System.IO;
using System.Linq;

using FMOD;

using Microsoft.Practices.Composite.Logging;
using Microsoft.Practices.ServiceLocation;

using Supremacy.Annotations;
using Supremacy.Collections;
using Supremacy.Resources;
using Supremacy.Types;

namespace Supremacy.Client
{
    public interface IAudioEngine : IDisposable
    {
        float MaxVolume { get; set; }
        void Stop();
        void Start();
        void SwitchMusicTrack(string trackName);
        void LoadMusic(MusicPack musicPack);
    }

    public class AudioEngine : IAudioEngine
    {
        private const float DefaultFadeStep = 0.025f;
	    private const string MusicDirectory = @"Resources\Music";

	    private readonly object _updateLock;

        #region AudioEngineTrack Class
        private sealed class AudioEngineTrack : IDisposable
        {
            // ReSharper disable MemberCanBePrivate.Local

            private readonly object _updateLock;
            private readonly Sound _sound;
            
            private Channel _channel;
            private float _maxVolume = 1.0f;
            private bool _isPlaying;
            private bool _isDisposed;
            private readonly string _trackName;

            public bool IsPlaying
            {
                get { return _isPlaying; }
            }

            public string TrackName
            {
                get { return _trackName; }
            }

            /// <summary>
            /// Gets or sets the current volume of the sound track.
            /// </summary>
            /// <value>The volume.</value>
            public float Volume
            {
                get
                {
                    CheckDisposed();
                    var volume = 0.0f;
                    if (_channel == null)
                        return volume;
                    ErrCheck(_channel.getVolume(ref volume));
                    return volume;
                }
                set
                {
                    lock (_updateLock)
                    {
                        CheckDisposed();
                        if (_channel == null)
                            throw new InvalidOperationException("Track must be initialized before setting volume.");
                        ErrCheck(_channel.setVolume(value));
                    }
                }
            }

            /// <summary>
            /// Gets or sets a value indicating whether this <see cref="AudioEngineTrack"/> is paused.
            /// </summary>
            /// <value><c>true</c> if paused; otherwise, <c>false</c>.</value>
            public bool IsPaused
            {
                get
                {
                    var paused = false;
                    ErrCheck(_channel.getPaused(ref paused));
                    return paused;
                }
                set
                {
                    lock (_updateLock)
                    {
                        ErrCheck(_channel.setPaused(value));
                    }
                }
            }

            public bool IsDisposed
            {
                get { return _isDisposed; }
            }

            /// <summary>
            /// Initializes a new instance of the <see cref="AudioEngineTrack"/> class.
            /// Loads the file and creates sound and channel
            /// </summary>
            public AudioEngineTrack([NotNull] FMOD.System system, [NotNull] MusicTrack track, [NotNull] object updateLock)
            {
                if (system == null)
                    throw new ArgumentNullException("system");
                if (track == null)
                    throw new ArgumentNullException("track");
                if (updateLock == null)
                    throw new ArgumentNullException("updateLock");
                _updateLock = updateLock;
                _trackName = track.TrackName;
                ErrCheck(
                    system.createSound(
                        ResourceManager.GetResourcePath(Path.Combine(MusicDirectory, track.FileName)),
                        MODE.HARDWARE | MODE.CREATESTREAM | MODE.LOOP_NORMAL,
                        ref _sound));
            }

            /// <summary>
            /// Plays the sound on a free channel
            /// </summary>
            /// <param name="system">The system.</param>
            public void Play(FMOD.System system)
            {
                CheckDisposed();
                lock (_updateLock)
                {
                    ErrCheck(system.playSound(
                                 CHANNELINDEX.FREE,
                                 _sound,
                                 true,
                                 ref _channel));
                    this.Volume = 0.0f;
                    this.IsPaused = false;
                    _isPlaying = true;    
                }            
            }

            /// <summary>
            /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
            /// </summary>
            public void Dispose()
            {
                if (_isDisposed)
                    return;
                try
                {
                    if (_isPlaying)
                        Stop();
                }
                finally
                {
                    try
                    {
                        ErrCheck(_sound.release());
                    }
                    finally
                    {
                        _isDisposed = true;
                    }
                }
            }

            public void FadeOut()
            {
                FadeOut(DefaultFadeStep);
            }

            /// <summary>
            /// Fadings the out track.
            /// </summary>
            /// <param name="fadeStep">The fade step</param>
            /// <returns>the volume was null</returns>
            public void FadeOut(float fadeStep)
            {
                CheckDisposed();
                lock (_updateLock)
                {
                    this.Volume = Math.Max(this.Volume - fadeStep, 0f);
                    if (this.Volume == 0.0f)
                        this.IsPaused = true;
                }
            }

            public void FadeIn()
            {
                FadeIn(DefaultFadeStep);
            }

            /// <summary>
            /// Fadings the in track.
            /// </summary>
            /// <param name="fadeStep">The fade step.</param>
            public void FadeIn(float fadeStep)
            {
                CheckDisposed();
                lock (_updateLock)
                {
                    this.Volume = Math.Min(this.Volume + fadeStep, _maxVolume);
                }
            }

            /// <summary>
            /// Limits the current sound to max volume.
            /// </summary>
            /// <param name="maxVolume">The max volume.</param>
            public void LimitToMaxVolume(float maxVolume)
            {
                CheckDisposed();
                lock (_updateLock)
                {
                    _maxVolume = maxVolume;
                    if (!_isPlaying)
                        return;
                    this.Volume = maxVolume;
                }
            }

            /// <summary>
            /// Stops the playing of the sound on this channel.
            /// </summary>
            public void Stop()
            {
                CheckDisposed();
                lock (_updateLock)
                {
                    if (_channel == null)
                        return;
                    ErrCheck(_channel.stop());
                    _channel = null;
                }
            }

            private void CheckDisposed()
            {
                if (_isDisposed)
                    throw new ObjectDisposedException("AudioEngineTrack");
            }

            private static void ErrCheck(RESULT result)
            {
                if (result == RESULT.OK)
                    return;
                throw new ApplicationException("FMOD error! " + result + " - " + Error.String(result));
            }
            // ReSharper restore MemberCanBePrivate.Local
        }   
        #endregion

	    private FMOD.System _system;

	    private readonly KeyedCollectionBase<string, AudioEngineTrack> _tracks;

	    private List<AudioEngineTrack> _previousTracks;
        private AudioEngineTrack _currentTrack;

        private float _maxVolume = 1.0f;
        //private Thread _updateThread;
	    private bool _isDisposed;
        private readonly IObservable<long> _updateTimer;
        private IDisposable _updateTimerSubscription;

        public AudioEngine()
        {
            _updateLock = new object();
            ServiceLocator.Current.GetInstance<ILoggerFacade>();
	        _previousTracks = new List<AudioEngineTrack>();
            _tracks = new KeyedCollectionBase<string, AudioEngineTrack>(track => track.TrackName);

            ErrCheck(Factory.System_Create(ref _system));
            uint version = 0;
            ErrCheck(_system.getVersion(ref version));

            if (version < VERSION.number)
            {
                throw new ApplicationException(
                    "Error! You are using an old version of FMOD "
                    + version.ToString("X")
                    + ". This program requires "
                    + VERSION.number.ToString("X") + ".");
            }
            ErrCheck(_system.init(16, INITFLAG.NORMAL, (IntPtr)null));
            //_updateThread = new Thread(DoUpdate);
            _updateTimer = Observable.Timer(TimeSpan.FromMilliseconds(50), TimeSpan.FromMilliseconds(50), new EventLoopScheduler("AudioEngine")).Do(_ => this.Update());
        }

	    public float MaxVolume
	    {
            get { return _maxVolume; }
            set
            {
                var previousMaxVolume = _maxVolume;
                _maxVolume = Math.Max(0.0f, Math.Min(value, 1.0f));
                if (FloatUtil.AreClose(previousMaxVolume, _maxVolume) && (_currentTrack != null))
                    _currentTrack.Volume = _maxVolume;
                OnMaxVolumeChanged();
            }
	    }

        private void OnMaxVolumeChanged()
        {
            foreach (var track in _tracks)
            {
                if (!track.IsDisposed)
                    track.LimitToMaxVolume(_maxVolume);
            }
        }

        private void ErrCheck(RESULT result)
		{
            if (result == RESULT.OK)
                return;

            lock (_updateLock)
            {
                if (_updateTimerSubscription == null)
                    return;
            }

            throw new ApplicationException("FMOD error! " + result + " - " + Error.String(result));
		}
		
        public void Stop()
        {
            lock (_updateLock)
            {
                if (_updateTimerSubscription == null)
                    return;

                _updateTimerSubscription.Dispose();
                _updateTimerSubscription = null;
/*
                if ((_updateThread != null) && _updateThread.IsAlive)
                {
                    //try { _updateThread.Interrupt(); }
                    //catch {}
                }
*/

                if (_currentTrack != null)
                {
                    try { _currentTrack.Stop(); }
                    catch {}
                }

                foreach (var previousTrack in _previousTracks)
                {
                    try { previousTrack.Stop(); }
                    catch {}
                }
            }
        }

        public void Start()
        {
            lock (_updateLock)
            {
                if (_updateTimerSubscription != null)
                    return;

                _updateTimerSubscription = _updateTimer.Subscribe();
            }
        }

        [DebuggerStepThrough]
        public void Update()
		{
		    lock (_updateLock)
		    {
		        if (_currentTrack != null)
		        {
		            if (_currentTrack.Volume < this.MaxVolume)
		                _currentTrack.FadeIn();
		        }

		        for (int i = 0; i < _previousTracks.Count; i++)
		        {
		            var previousTrack = _previousTracks[i];
		            previousTrack.FadeOut();
		            if (previousTrack.Volume <= 0.0f)
		                _previousTracks.RemoveAt(i--);
		        }
            
		        _system.update();
		    }
		}

        public void SwitchMusicTrack(string trackName)
        {
            AudioEngineTrack track;
            
            if (!_tracks.TryGetValue(trackName, out track))
                return;

            lock (_updateLock)
            {
                _previousTracks.Remove(track);
                if (_currentTrack != null && _currentTrack != track)
                    _previousTracks.Add(_currentTrack);
                _currentTrack = track;
                if (!track.IsPlaying)
                    track.Play(_system);
                else if (track.IsPaused)
                    track.IsPaused = false;
            }
        }

        public void LoadMusic(MusicPack musicPack)
        {   
            lock (_updateLock)
            {
                var wasRunning = (_updateTimerSubscription != null);
                if (wasRunning)
                    Stop();

                ClearTracks();

                foreach (var musicTrack in musicPack.Tracks)
                {
                    try { _tracks.Add(new AudioEngineTrack(_system, musicTrack, _updateLock)); }
                    catch {}
                }

                if (wasRunning)
                    Start();
            }
        }

        private void ClearTracks()
        {
            try
            {
                _currentTrack = null;
                for (int i = 0; i < _tracks.Count; i++)
                {
                    var track = _tracks[i];
                    if (!track.IsDisposed)
                        track.Dispose();
                    _tracks.RemoveAt(i--);
                }
            }
            catch {}
        }

	    public void Dispose()
	    {
	        if (_isDisposed)
	            return;

	        _isDisposed = true;

            lock (_updateLock)
            {
                try { Stop(); }
                catch {}

                try { ClearTracks(); }
                catch {}

                try
                {
                    if (_system != null)
                        _system.release();
                    _system = null;
                }
                catch {}
            }
	    }
    }
}
